//  (c)2012 Henrik Nilsson
//  http://idmcrisis.com

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Net;
using System.Security;
using Microsoft.MetadirectoryServices;
using SevenZip;

namespace IdmCrisis.Fim.Connectors
{
    /// <summary>
    /// FIM ECMA File connector for managing remote files on Filesystem. FTP and SFTP. It also supports compression and encryption of files.
    /// </summary>
    public class UltimateFileConnector : IMAExtensible2FileImport, IMAExtensible2FileExport, IMAExtensible2GetCapabilities, IMAExtensible2GetParameters
    {
        // Class holding UI config parameter strings. 
        private struct Param
        {
            public const string RemoteLocation = @"Remote Location";

            public const string FullPath = "Full Import/Export Uri/File Path";
            public const string DeltaPath = "Delta Import/Export Uri/File Path";
            public const string UserName = "User Name";
            public const string Password = "Password";
            public const string Domain = "Domain";

            public const string UsePassive = "Use Passive Mode";
            public const string EnableSsl = "Enable SSL/TLS";
            public const string UseBinary = "Use Binary Transfer Mode";

            public const string AuthenticationMethod = "Authentication Method";
            public const string PrivateKeyFile = "Private Key File";
            public const string PassPhrase = "Pass Phrase";

            public const string UseCompression = "Use Compression";
            public const string ArchiveFormat = "Archive Format";
            public const string FullArchiveInternalFileName = "Full Import/Export Archive Internal File Name";
            public const string DeltaArchiveInternalFileName = "Delta Import/Export Archive Internal File Name";

            public const string UseEncryption = "Use Encryption";
            public const string ArchivePassword = "Archive Password";
            public const string ZipEncryptionMethod = "Zip Encryption Method";
        }
        
        // Required location of 7z.dll file.
        private readonly string zipLibraryPath = Path.Combine(Utils.ExtensionsDirectory, "7z.dll");
        
        /// <summary>
        /// Downloads(FTP, SFTP) or copies(File) a file to MAData folder for import.
        /// </summary>
        public WriteImportFileResults WriteImportFile(KeyedCollection<string, ConfigParameter> config, Schema types, WriteImportFileRunStep importRunStep)
        {
            // NetworkCredential for connecting to remote file.
            var networkCredential = new NetworkCredential(config[Param.UserName].Value,
                                                          config[Param.Password].SecureValue,
                                                          config[Param.Domain].Value);

            // Determine which path to use.
            var path = importRunStep.ImportType == OperationType.Full ? 
                config[Param.FullPath].Value : 
                config[Param.DeltaPath].Value;

            // Init Remote File object.
            var remoteFile = RemoteFile.Create(new Uri(path), importRunStep.FilePath, networkCredential);

            // Compression and Encryption Settings.
            var archiveFileName = importRunStep.ImportType == OperationType.Full ?
               config[Param.FullArchiveInternalFileName].Value :
               config[Param.DeltaArchiveInternalFileName].Value;

            SecureString archivePassword = null;
            if(config.Contains(Param.ArchivePassword))
                archivePassword = config[Param.ArchivePassword].SecureValue;

            // Get file, extract and save.
            switch (config[Param.RemoteLocation].ToEnum<RemoteLocation>())
            {
                case RemoteLocation.Ftp:
                {
                    remoteFile
                        .DownloadFromFtp(config[Param.UsePassive].ToBool(), 
                                            config[Param.EnableSsl].ToBool(), 
                                            config[Param.UseBinary].ToBool())
                        .Extract(config[Param.UseCompression].ToBool(),
                                    archiveFileName,
                                    config[Param.UseEncryption].ToBool(),
                                    archivePassword, 
                                    zipLibraryPath)
                        .WriteLocal();

                    break;
                }
                case RemoteLocation.Scp:
                case RemoteLocation.SFtp :
                {
                    // SFTP/SCP specific settings.
                    var sFtpAuthentiationMethod = config[Param.AuthenticationMethod].ToEnum<SshAuthenticationMethod>();

                    SecureString passPhrase = null;
                    
                    var privateKey = config[Param.PrivateKeyFile].ToByteArray();
                    if (config.Contains(Param.PassPhrase))
                            passPhrase = config[Param.PassPhrase].SecureValue;
                    
                    switch (config[Param.RemoteLocation].ToEnum<RemoteLocation>())
                    {
                        case RemoteLocation.SFtp:
                            remoteFile
                                .DownloadFromSFtp(sFtpAuthentiationMethod,
                                                    privateKey, 
                                                    passPhrase)
                                .Extract(config[Param.UseCompression].ToBool(),
                                            archiveFileName,
                                            config[Param.UseEncryption].ToBool(),
                                            archivePassword, 
                                            zipLibraryPath)
                                .WriteLocal();
                            break;
                        case RemoteLocation.Scp:
                            remoteFile
                                .DownloadFromScp(sFtpAuthentiationMethod, 
                                                    privateKey, 
                                                    passPhrase)
                                .Extract(config[Param.UseCompression].ToBool(),
                                            archiveFileName,
                                            config[Param.UseEncryption].ToBool(),
                                            archivePassword, 
                                            zipLibraryPath)
                                .WriteLocal();
                            break;
                    }

                    break;
                }
                case RemoteLocation.File:
                {
                    remoteFile
                        .OpenFromFileSystem()
                        .Extract(config[Param.UseCompression].ToBool(),
                                    archiveFileName,
                                    config[Param.UseEncryption].ToBool(),
                                    archivePassword, 
                                    zipLibraryPath)
                        .WriteLocal();

                    break;
                }
            }

            return new WriteImportFileResults();
        }

        /// <summary>
        /// Uploads(FTP, SFTP) or copies the(File) a file to target location for export.
        /// </summary>
        public void ReadExportFile(KeyedCollection<string, ConfigParameter> config, Schema types, ReadExportFileRunStep exportRunStep)
        {
            // Create NetworkCredential.
            var networkCredential = new NetworkCredential(config[Param.UserName].Value,
                                                          config[Param.Password].SecureValue,
                                                          config[Param.Domain].Value);

            // Determine which path to use.
            var path = exportRunStep.ExportType == OperationType.Full ?
                config[Param.FullPath].Value :
                config[Param.DeltaPath].Value;

            // Init Remote File object.
            var remoteFile = RemoteFile.Create(new Uri(path), exportRunStep.FilePath, networkCredential);

            // Compression and Encryption Settings.
            var archiveFileName = exportRunStep.ExportType == OperationType.Full ?
               config[Param.FullArchiveInternalFileName].Value :
               config[Param.DeltaArchiveInternalFileName].Value;

            SecureString archivePassword = null;
            if(config.Contains(Param.ArchivePassword))
                archivePassword = config[Param.ArchivePassword].SecureValue;

            // Encrypt, Compress and upload file.
            switch (config[Param.RemoteLocation].ToEnum<RemoteLocation>())
            {
                case RemoteLocation.Ftp:
                {
                    // Encrypt, compress and upload file. 
                    remoteFile
                        .ReadLocal()
                        .Compress(config[Param.UseCompression].ToBool(),
                                    config[Param.ArchiveFormat].ToEnum<OutArchiveFormat>(),
                                    archiveFileName, 
                                    config[Param.UseEncryption].ToBool(), 
                                    archivePassword,
                                    config[Param.ZipEncryptionMethod].ToEnum<ZipEncryptionMethod>(), 
                                    zipLibraryPath)
                        .UploadToFtp(config[Param.UsePassive].ToBool(), config[Param.EnableSsl].ToBool(), config[Param.UseBinary].ToBool());
                    
                    break;
                }
                case RemoteLocation.Scp:
                case RemoteLocation.SFtp:
                {
                    // SFTP/SCP specific settings.
                    var sFtpAuthentiationMethod = config[Param.AuthenticationMethod].ToEnum<SshAuthenticationMethod>();

                    byte[] privateKey = null;
                    SecureString passPhrase = null;
                    if (sFtpAuthentiationMethod == SshAuthenticationMethod.PrivateKey)
                    {
                        privateKey = File.ReadAllBytes(config[Param.PrivateKeyFile].Value);

                        if (config.Contains(Param.PassPhrase))
                            passPhrase = config[Param.PassPhrase].SecureValue;
                    }

                    // Encrypt, compress and upload file.
                    switch (config[Param.RemoteLocation].ToEnum<RemoteLocation>())
                    {
                        case RemoteLocation.SFtp:
                            remoteFile
                                .ReadLocal()
                                .Compress(config[Param.UseCompression].ToBool(),
                                    config[Param.ArchiveFormat].ToEnum<OutArchiveFormat>(),
                                    archiveFileName,
                                    config[Param.UseEncryption].ToBool(),
                                    archivePassword,
                                    config[Param.ZipEncryptionMethod].ToEnum<ZipEncryptionMethod>(),
                                    zipLibraryPath)
                                .UploadToSFtp(sFtpAuthentiationMethod, privateKey, passPhrase);
                            break;
                        case RemoteLocation.Scp:
                            remoteFile
                                .ReadLocal()
                                .Compress(config[Param.UseCompression].ToBool(),
                                    config[Param.ArchiveFormat].ToEnum<OutArchiveFormat>(),
                                    archiveFileName,
                                    config[Param.UseEncryption].ToBool(),
                                    archivePassword,
                                    config[Param.ZipEncryptionMethod].ToEnum<ZipEncryptionMethod>(),
                                    zipLibraryPath)
                                .UploadToScp(sFtpAuthentiationMethod, privateKey, passPhrase);
                            break;
                    }

                    break;
                }
                case RemoteLocation.File:
                {
                    // Encrypt, compress and upload file. 
                    remoteFile
                        .ReadLocal()
                        .Compress(config[Param.UseCompression].ToBool(),
                                    config[Param.ArchiveFormat].ToEnum<OutArchiveFormat>(),
                                    archiveFileName,
                                    config[Param.UseEncryption].ToBool(),
                                    archivePassword,
                                    config[Param.ZipEncryptionMethod].ToEnum<ZipEncryptionMethod>(),
                                    zipLibraryPath)
                        .SaveToFileSystem();

                    break;
                }
            }
        }

        /// <summary>
        /// Return a set of capabilites supported by this connector.
        /// </summary>
        public MACapabilities Capabilities
        {
            get { return new MACapabilities
            {
                ConcurrentOperation = true,
                ObjectRename = false,
                DeleteAddAsReplace = true,
                DeltaImport = true,
                DistinguishedNameStyle = MADistinguishedNameStyle.None,
                ExportType = MAExportType.AttributeUpdate,
                NoReferenceValuesInFirstExport = false,
                FullExport = true,
                Normalizations = MANormalizations.None
            }; }
        }

        /// <summary>
        /// Create the UI specific for this connector.
        /// </summary>
        public IList<ConfigParameterDefinition> GetConfigParameters(KeyedCollection<string, ConfigParameter> configParameters, ConfigParameterPage page)
        {
            var configParams = new List<ConfigParameterDefinition>();
                        
            if(page == ConfigParameterPage.Connectivity)
            {
                var remoteLocations = string.Join(",", Enum.GetNames(typeof (RemoteLocation)));
                var outArchiveFormats = string.Join(",", Enum.GetNames(typeof(OutArchiveFormat)));
                var sFtpAuthenticationMethods = string.Join(",", Enum.GetNames(typeof (SshAuthenticationMethod)));
                var zipEncryptionMethods = string.Join(",", Enum.GetNames(typeof(ZipEncryptionMethod)));
                
                configParams.AddRange(new[]
                {
                    ConfigParameterDefinition.CreateLabelParameter("Connectivity"),
                    ConfigParameterDefinition.CreateDropDownParameter(Param.RemoteLocation, remoteLocations, false, RemoteLocation.Ftp.ToString()),
                    ConfigParameterDefinition.CreateStringParameter(Param.FullPath, string.Empty),
                    ConfigParameterDefinition.CreateStringParameter(Param.DeltaPath, string.Empty),
                    ConfigParameterDefinition.CreateLabelParameter("Uri/File Path allows inserting parts of current date and time by using format specifiers between ?? according to the specification found at http://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx. Example: ftp://site/?yyyy-MM-dd?.txt"),

                    ConfigParameterDefinition.CreateDividerParameter(),
                    ConfigParameterDefinition.CreateLabelParameter("Authentication (All Locations)"),
                    ConfigParameterDefinition.CreateStringParameter(Param.UserName, string.Empty),
                    ConfigParameterDefinition.CreateEncryptedStringParameter(Param.Password, string.Empty),
                    ConfigParameterDefinition.CreateStringParameter(Param.Domain, string.Empty),
                    ConfigParameterDefinition.CreateLabelParameter("Specifying credentials for File System location will impersonate reads and writes. Domain applies to File System location only."),

                    ConfigParameterDefinition.CreateDividerParameter(),
                    ConfigParameterDefinition.CreateLabelParameter("FTP Settings (FTP Location only)"),
                    ConfigParameterDefinition.CreateCheckBoxParameter(Param.UsePassive, true),
                    ConfigParameterDefinition.CreateCheckBoxParameter(Param.EnableSsl, false),
                    ConfigParameterDefinition.CreateCheckBoxParameter(Param.UseBinary, false),
                    
                    ConfigParameterDefinition.CreateDividerParameter(),
                    ConfigParameterDefinition.CreateLabelParameter("SFTP and SCP Settings (SFTP and SCP Locations only)"),
                    ConfigParameterDefinition.CreateDropDownParameter(Param.AuthenticationMethod, sFtpAuthenticationMethods, false, SshAuthenticationMethod.Password.ToString()),
                    ConfigParameterDefinition.CreateFileParameter(Param.PrivateKeyFile),
                    ConfigParameterDefinition.CreateEncryptedStringParameter(Param.PassPhrase, string.Empty),

                    ConfigParameterDefinition.CreateDividerParameter(),
                    ConfigParameterDefinition.CreateLabelParameter("File Compression Settings (All Locations)")
                });

                if (File.Exists(zipLibraryPath))
                {
                    configParams.AddRange(new[]
                    {
                        ConfigParameterDefinition.CreateCheckBoxParameter(Param.UseCompression, false),
                        ConfigParameterDefinition.CreateStringParameter(Param.FullArchiveInternalFileName, string.Empty),
                        ConfigParameterDefinition.CreateStringParameter(Param.DeltaArchiveInternalFileName, string.Empty),
                        ConfigParameterDefinition.CreateDropDownParameter(Param.ArchiveFormat, outArchiveFormats, false, OutArchiveFormat.Zip.ToString()),

                        ConfigParameterDefinition.CreateDividerParameter(),
                        ConfigParameterDefinition.CreateLabelParameter("File Encryption Settings, applies to Zip and SevenZip formats only."),
                        ConfigParameterDefinition.CreateCheckBoxParameter(Param.UseEncryption, false),
                        ConfigParameterDefinition.CreateEncryptedStringParameter(Param.ArchivePassword, string.Empty),
                        ConfigParameterDefinition.CreateDropDownParameter(Param.ZipEncryptionMethod, zipEncryptionMethods, false, ZipEncryptionMethod.Aes256.ToString())
                    });
                }
                else
                {
                    configParams.Add(
                        ConfigParameterDefinition.CreateLabelParameter(
                            string.Format("This connector supports file compression/extraction for a lot of archive formats " +
                                          "but relies on the functionality of 7-Zip http://www.7-zip.org/.\n\n" +
                                          "In order to use compression/extraction with this connector, " +
                                          "download and install 7-Zip and copy the 7z.dll file to the Extensions folder:\n{0}\n" +
                                          "Finally refresh the interfaces of this connector to be able to set compression/extraction settings.\n\n", Utils.ExtensionsDirectory)));
                }

            }
            return configParams;   
        }

        /// <summary>
        /// Validate user input.
        /// </summary>
        public ParameterValidationResult ValidateConfigParameters(KeyedCollection<string, ConfigParameter> config, ConfigParameterPage page)
        {
            // Full Path.
            if (string.IsNullOrEmpty(config[Param.FullPath].Value))
                return new ParameterValidationResult(ParameterValidationResultCode.Failure, 
                    string.Format("You must supply '{0}'.", Param.FullPath), 
                    Param.FullPath);

            // Delta Path.
            if (string.IsNullOrEmpty(config[Param.DeltaPath].Value))
                return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                    string.Format("You must supply '{0}'.", Param.DeltaPath),
                    Param.DeltaPath);

            // Test uri validity.
            if (config[Param.RemoteLocation].Value != RemoteLocation.File.ToString())
            {

                if (!Uri.IsWellFormedUriString(config[Param.FullPath].Value.SubstituteDateTimeFormat(DateTime.Now), UriKind.RelativeOrAbsolute))
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("The value for '{0}' is invalid (Note that SFTP and SCP also requires a Uri Scheme for example 'sftp://' even thought it won't be used).", Param.FullPath),
                        Param.FullPath);

                if (!Uri.IsWellFormedUriString(config[Param.DeltaPath].Value.SubstituteDateTimeFormat(DateTime.Now), UriKind.Absolute))
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("The value for '{0}' is invalid (Note that SFTP and SCP also requires a Uri Scheme for example 'sftp://' even thought it won't be used).", Param.DeltaPath),
                        Param.DeltaPath);

            }

            // FTP.
            if (config[Param.RemoteLocation].Value == RemoteLocation.Ftp.ToString())
            {
                // User Name.
                if (string.IsNullOrEmpty(config[Param.UserName].Value))
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("You must supply '{0}'.", Param.UserName),
                        Param.UserName);
                
                // Full Path.
                if (new Uri(config[Param.FullPath].Value).Scheme != Uri.UriSchemeFtp)
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("Invalid Uri Scheme for '{0}', FTP location should have 'ftp://' as Uri Scheme.", Param.FullPath),
                        Param.FullPath);

                // Delta Path.
                if (new Uri(config[Param.DeltaPath].Value).Scheme != Uri.UriSchemeFtp)
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("Invalid Uri Scheme for '{0}', FTP location should have 'ftp://' as Uri Scheme.", Param.DeltaPath),
                        Param.DeltaPath);
            }

            // SFTP/SCP (SSH)
            if (config[Param.RemoteLocation].Value == RemoteLocation.SFtp.ToString() || config[Param.RemoteLocation].Value == RemoteLocation.Scp.ToString())
            {
                // User Name.
                if (string.IsNullOrEmpty(config[Param.UserName].Value))
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("You must supply '{0}'.", Param.UserName),
                        Param.UserName);
                
                // Password
                if( config[Param.AuthenticationMethod].Value.ToEnum<SshAuthenticationMethod>() == SshAuthenticationMethod.Password)
                {
                    if(!config.Contains(Param.Password))
                        return new ParameterValidationResult(ParameterValidationResultCode.Failure, 
                            string.Format("You must supply {0} when using {1} 'Password'.", Param.Password, Param.AuthenticationMethod) , 
                            Param.Password);

                    if(config[Param.Password].SecureValue.Length == 0)
                        return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                            string.Format("You must supply {0} when using {1} 'Password'.", Param.Password, Param.AuthenticationMethod), 
                            Param.Password);
                }

                // Private Key File.
                if (config[Param.AuthenticationMethod].Value.ToEnum<SshAuthenticationMethod>() == SshAuthenticationMethod.PrivateKey)
                {
                    if (config[Param.PrivateKeyFile] == null)
                        return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                            string.Format("You must supply '{0}' when using SFTP/SCP authentication mode 'PrivateKey'.", Param.PrivateKeyFile),
                            Param.PrivateKeyFile);

                    if (config[Param.PrivateKeyFile].Value.Length == 0)
                        return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                            string.Format("You must supply '{0}' when using SFTP/SCP authentication mode 'PrivateKey'.", Param.PrivateKeyFile),
                            Param.PrivateKeyFile);
                }
            }

            // File System.
            if (config[Param.RemoteLocation].Value == RemoteLocation.File.ToString())
            {
                // Domain.
                if (string.IsNullOrEmpty(config[Param.Domain].Value) && !string.IsNullOrEmpty(config[Param.UserName].Value))
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("'{0}' is required when using {1} as {2} and {3} is specified.", Param.Domain, RemoteLocation.File, Param.RemoteLocation, Param.UserName),
                        Param.Domain);
            }

            // Compression
            if(config[Param.UseCompression].ToBool())
            {
                // Full Archive Internal File Name.
                if (string.IsNullOrEmpty(config[Param.FullArchiveInternalFileName].Value))
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("The value for '{0}' must be specified when '{1} is selected.", Param.FullArchiveInternalFileName, Param.UseCompression),
                        Param.FullArchiveInternalFileName);

                if (config[Param.FullArchiveInternalFileName]
                        .Value
                        .SubstituteDateTimeFormat()
                        .ContainsAny(Path.GetInvalidFileNameChars()))
                {
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("The value for '{0}' is an invalid file name.", Param.FullArchiveInternalFileName),
                        Param.FullArchiveInternalFileName);
                }

                // Delta Archive Internal File Name.
                if (string.IsNullOrEmpty(config[Param.DeltaArchiveInternalFileName].Value))
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("The value for '{0}' must be specified when '{1}' is selected.", Param.DeltaArchiveInternalFileName, Param.UseCompression),
                        Param.DeltaArchiveInternalFileName);

                if (config[Param.DeltaArchiveInternalFileName]
                        .Value
                        .SubstituteDateTimeFormat()
                        .ContainsAny(Path.GetInvalidFileNameChars()))
                {
                    return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                        string.Format("The value for '{0}' is an invalid file name.", Param.DeltaArchiveInternalFileName),
                        Param.DeltaArchiveInternalFileName);
                }

                
                if(config[Param.UseEncryption].ToBool())
                {
                    // Archive Format
                    if (config[Param.ArchiveFormat].Value.ToEnum<OutArchiveFormat>() != OutArchiveFormat.Zip ||
                        config[Param.ArchiveFormat].Value.ToEnum<OutArchiveFormat>() != OutArchiveFormat.SevenZip)
                    {
                        return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                            string.Format("Only the archive formats that supports encryption is {0} and {1}.", OutArchiveFormat.Zip, OutArchiveFormat.SevenZip),
                            Param.ArchiveFormat);
                    }

                    // Archive Password.
                    if (config[Param.ArchivePassword] == null || config[Param.ArchivePassword].SecureValue.Length == 0)
                    {
                        return new ParameterValidationResult(ParameterValidationResultCode.Failure,
                            string.Format("You must supply '{0}' when using Encryption.", Param.ArchivePassword),
                            Param.ArchivePassword);
                    }
                }
            }

            return new ParameterValidationResult(ParameterValidationResultCode.Success,null,null);
        }

    }
}
